]> dgit.raspbian.org Git - ostree.git/commitdiff
bootconfig: Preserve extension BLS keys across staged deployments
authorJoseph Marrero Corchado <jmarrero@redhat.com>
Wed, 1 Apr 2026 19:40:45 +0000 (15:40 -0400)
committerJoseph Marrero Corchado <jmarrero@redhat.com>
Thu, 2 Apr 2026 00:52:59 +0000 (20:52 -0400)
When a deployment is staged via ostree_sysroot_stage_tree_with_options(),
the deployment metadata is serialized to /run/ostree/staged-deployment
as a GVariant. During finalization at shutdown,
_ostree_sysroot_reload_staged() creates a fresh OstreeBootconfigParser
and only restores the "options" key from the serialized kargs. Any
additional BLS keys that were set on the bootconfig are silently dropped.

The parse/write/clone paths in OstreeBootconfigParser already handle
unknown keys generically (the "Write unknown fields" loop in
write_at()), so keys survive direct deployments and in-memory
operations. The gap is exclusively in the staged deployment roundtrip,
where a fresh bootconfig is rebuilt from just the kargs strv.

This matters for the upcoming bootc `loader-entries set-options-for-source`
feature, which stores kernel argument ownership as extension BLS keys
(e.g. `x-options-source-tuned nohz=full isolcpus=1-3`). On bootc
systems with transient /etc, tools like TuneD lose track of which kargs
they own because their state files are wiped on reboot. Tracking
ownership directly in the BLS config on /boot solves this, but only if
the keys survive staging. systemd-boot, GRUB, and zipl all ignore
unknown BLS keys, so extension keys are safe.

Fix this by following the same pattern used for overlay-initrds:

1. Add _ostree_bootconfig_parser_get_extra_keys_variant() which returns
   all non-standard BLS keys as an a{ss} GVariant. Standard keys
   (title, version, options, linux, initrd, devicetree) are excluded
   since they are rebuilt from scratch during finalization. All other
   keys are preserved, trusting the caller.

2. In ostree_sysroot_stage_tree_with_options(), serialize any extra
   keys as "bootconfig-extra" in the staged GVariant dict. Since
   _ostree_deployment_set_bootconfig_from_kargs() creates a fresh
   bootconfig with only the "options" key, the code falls back to
   the merge deployment's bootconfig for extra keys. This ensures
   keys are inherited across staged deployments without the caller
   needing to re-set them.

3. In _ostree_sysroot_reload_staged(), restore extra keys from the
   "bootconfig-extra" dict onto the deployment's bootconfig via
   ostree_bootconfig_parser_set().

The function is private (_ostree_ prefix) since only ostree's own
staging code uses it. No new public API, no changes to .sym files,
no changes to GIR or Rust bindings.

Backwards compatibility:
- Old ostree ignores the unknown "bootconfig-extra" key in the a{sv}
  dict (extension keys silently lost, same as before this patch).
- New ostree gracefully handles the absence of "bootconfig-extra" in
  staged data written by older versions (g_variant_dict_lookup returns
  FALSE, no restoration attempted).

Assisted-by: OpenCode (Claude claude-opus-4-6)
Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
src/libostree/ostree-bootconfig-parser-private.h
src/libostree/ostree-bootconfig-parser.c
src/libostree/ostree-sysroot-deploy.c
src/libostree/ostree-sysroot.c
tests/test-bootconfig-parser-internals.c

index 16ccb0fcdc781adbddfaabf0ea85b53c9d3fc9de..1450fa992124e2d4af4bd5a1de5fdeaa43832a5f 100644 (file)
@@ -8,4 +8,6 @@ G_BEGIN_DECLS
 
 const char *_ostree_bootconfig_parser_filename (OstreeBootconfigParser *self);
 
+GVariant *_ostree_bootconfig_parser_get_extra_keys_variant (OstreeBootconfigParser *self);
+
 G_END_DECLS
index 2e08256d7207c3a6502817aa60a3717b16a084e0..4e7c823a597c86400b180076418f7f086e3bad91 100644 (file)
@@ -339,6 +339,60 @@ ostree_bootconfig_parser_write (OstreeBootconfigParser *self, GFile *output,
                                             cancellable, error);
 }
 
+/* Standard BLS keys that are managed by ostree's own deployment code.
+ * These are rebuilt from scratch during staged deployment finalization
+ * (title, version, linux, initrd from the deployment; options from the
+ * serialized kargs), so they must NOT be duplicated into bootconfig-extra.
+ */
+static const char *const standard_bls_keys[]
+    = { "title", "version", "options", "linux", "initrd", "devicetree", NULL };
+
+static gboolean
+is_standard_bls_key (const char *key)
+{
+  for (const char *const *p = standard_bls_keys; *p != NULL; p++)
+    {
+      if (strcmp (key, *p) == 0)
+        return TRUE;
+    }
+  return FALSE;
+}
+
+/**
+ * _ostree_bootconfig_parser_get_extra_keys_variant:
+ * @self: Parser
+ *
+ * Returns a GVariant of type "a{ss}" containing all bootconfig keys
+ * that are not part of the standard BLS set managed by ostree.  These
+ * are extension keys set by consumers like bootc (e.g.
+ * "x-options-source-tuned") that need to survive the staged deployment
+ * serialization roundtrip.
+ *
+ * Returns: (transfer full) (nullable): A new floating GVariant, or NULL if
+ *          there are no extra keys
+ */
+GVariant *
+_ostree_bootconfig_parser_get_extra_keys_variant (OstreeBootconfigParser *self)
+{
+  g_auto (GVariantBuilder) builder = OT_VARIANT_BUILDER_INITIALIZER;
+  gboolean has_entries = FALSE;
+
+  g_variant_builder_init (&builder, (GVariantType *)"a{ss}");
+
+  GLNX_HASH_TABLE_FOREACH_KV (self->options, const char *, k, const char *, v)
+    {
+      if (is_standard_bls_key (k))
+        continue;
+      g_variant_builder_add (&builder, "{ss}", k, v);
+      has_entries = TRUE;
+    }
+
+  if (!has_entries)
+    return NULL;
+
+  return g_variant_builder_end (&builder);
+}
+
 static void
 ostree_bootconfig_parser_finalize (GObject *object)
 {
index 1dbaaa7e9d36a3c096b0cf0c93bac18fd4ba93c1..77b601ff42275da3750c7a7f46b785fc5c1630d4 100644 (file)
@@ -3884,6 +3884,33 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname,
     g_variant_builder_add (builder, "{sv}", "overlay-initrds",
                            g_variant_new_strv ((const char *const *)opts->overlay_initrds, -1));
 
+  /* Serialize any extension BLS keys (e.g. x-options-source-tuned).
+   * These are custom keys set by consumers like bootc and need to survive
+   * the staging roundtrip so they are preserved during finalization at shutdown.
+   *
+   * First check the new deployment's bootconfig (in case the caller set keys
+   * on it directly).  If none found, fall back to the merge deployment's
+   * bootconfig, which carries the keys from the currently deployed BLS entry.
+   * This ensures that x-prefixed keys are inherited across staged deployments
+   * even though _ostree_deployment_set_bootconfig_from_kargs() creates a fresh
+   * bootconfig containing only the "options" key.
+   */
+  {
+    GVariant *extra = NULL;
+    OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment);
+    if (bootconfig)
+      extra = _ostree_bootconfig_parser_get_extra_keys_variant (bootconfig);
+    if (!extra && merge_deployment)
+      {
+        OstreeBootconfigParser *merge_bootconfig
+            = ostree_deployment_get_bootconfig (merge_deployment);
+        if (merge_bootconfig)
+          extra = _ostree_bootconfig_parser_get_extra_keys_variant (merge_bootconfig);
+      }
+    if (extra)
+      g_variant_builder_add (builder, "{sv}", "bootconfig-extra", extra);
+  }
+
   const char *parent = dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED));
   if (!glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, cancellable, error))
     return FALSE;
index 282df6d100c709c121d46b180d112cbd23e1c42f..5b837bb2f798256abca8a7baaacd6514bc7a0777 100644 (file)
@@ -1225,6 +1225,24 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error)
 
           _ostree_deployment_set_overlay_initrds (staged, overlay_initrds);
 
+          /* Restore any extension BLS keys (e.g. x-options-source-tuned)
+           * that were serialized during staging. This preserves custom keys
+           * set by consumers like bootc through the staging roundtrip.
+           */
+          {
+            g_autoptr (GVariant) bootconfig_extra = NULL;
+            if (g_variant_dict_lookup (staged_deployment_dict, "bootconfig-extra", "@a{ss}",
+                                       &bootconfig_extra))
+              {
+                OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (staged);
+                GVariantIter iter;
+                const char *key, *value;
+                g_variant_iter_init (&iter, bootconfig_extra);
+                while (g_variant_iter_next (&iter, "{&s&s}", &key, &value))
+                  ostree_bootconfig_parser_set (bootconfig, key, value);
+              }
+          }
+
           self->staged_deployment = g_steal_pointer (&staged);
           self->staged_deployment_data = g_steal_pointer (&staged_deployment_data);
           /* We set this flag for ostree_deployment_is_staged() because that API
index 00b18d4a172494d01065a5ab6a6658993161810c..d84c4a3be0eb07429e02c49f5a0533679a9de6dc 100644 (file)
@@ -49,6 +49,211 @@ test_parse_tries_invalid (void)
   g_assert_cmpuint (done, ==, 0);
 }
 
+static void
+test_extra_keys_variant_empty (void)
+{
+  /* A bootconfig with no x-prefixed keys should return NULL */
+  OstreeBootconfigParser *parser = ostree_bootconfig_parser_new ();
+  ostree_bootconfig_parser_set (parser, "title", "Fedora Linux 43");
+  ostree_bootconfig_parser_set (parser, "version", "1");
+  ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc rw quiet");
+  ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz-6.8.0");
+  ostree_bootconfig_parser_set (parser, "initrd", "/initramfs-6.8.0.img");
+
+  GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser);
+  g_assert_null (extra);
+
+  g_object_unref (parser);
+}
+
+static void
+test_extra_keys_variant_with_extension_keys (void)
+{
+  /* Standard keys should be excluded, only extension keys returned */
+  OstreeBootconfigParser *parser = ostree_bootconfig_parser_new ();
+  ostree_bootconfig_parser_set (parser, "title", "Fedora Linux 43");
+  ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc rw");
+  ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz-6.8.0");
+  ostree_bootconfig_parser_set (parser, "x-options-source-tuned", "nohz=full isolcpus=1-3");
+  ostree_bootconfig_parser_set (parser, "x-options-source-dracut", "rd.driver.pre=vfio-pci");
+
+  GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser);
+  g_assert_nonnull (extra);
+
+  GVariant *extra_owned = g_variant_ref_sink (extra);
+
+  /* Should be a{ss} with exactly 2 entries */
+  g_assert_true (g_variant_is_of_type (extra_owned, G_VARIANT_TYPE ("a{ss}")));
+  g_assert_cmpuint (g_variant_n_children (extra_owned), ==, 2);
+
+  /* Verify the contents */
+  GVariantIter iter;
+  const char *key, *val;
+  gboolean found_tuned = FALSE, found_dracut = FALSE;
+  g_variant_iter_init (&iter, extra_owned);
+  while (g_variant_iter_next (&iter, "{&s&s}", &key, &val))
+    {
+      if (g_str_equal (key, "x-options-source-tuned"))
+        {
+          g_assert_cmpstr (val, ==, "nohz=full isolcpus=1-3");
+          found_tuned = TRUE;
+        }
+      else if (g_str_equal (key, "x-options-source-dracut"))
+        {
+          g_assert_cmpstr (val, ==, "rd.driver.pre=vfio-pci");
+          found_dracut = TRUE;
+        }
+      else
+        {
+          g_assert_not_reached ();
+        }
+    }
+  g_assert_true (found_tuned);
+  g_assert_true (found_dracut);
+
+  g_variant_unref (extra_owned);
+  g_object_unref (parser);
+}
+
+static void
+test_extra_keys_variant_standard_excluded (void)
+{
+  /* Standard BLS keys (title, version, options, linux, initrd, devicetree)
+   * should be excluded. All other keys should be preserved.
+   */
+  OstreeBootconfigParser *parser = ostree_bootconfig_parser_new ();
+  ostree_bootconfig_parser_set (parser, "title", "Test");
+  ostree_bootconfig_parser_set (parser, "version", "1.0");
+  ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc");
+  ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz");
+  ostree_bootconfig_parser_set (parser, "initrd", "/initramfs.img");
+  ostree_bootconfig_parser_set (parser, "devicetree", "/dtb");
+
+  /* Only standard keys -- should return NULL */
+  GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser);
+  g_assert_null (extra);
+
+  /* Add non-standard keys -- all should be preserved */
+  ostree_bootconfig_parser_set (parser, "my-custom-key", "some-value");
+  ostree_bootconfig_parser_set (parser, "x-options-source-tuned", "nohz=full");
+
+  extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser);
+  g_assert_nonnull (extra);
+  GVariant *extra_owned = g_variant_ref_sink (extra);
+
+  g_assert_cmpuint (g_variant_n_children (extra_owned), ==, 2);
+
+  GVariantIter iter;
+  const char *key, *val;
+  gboolean found_custom = FALSE, found_tuned = FALSE;
+  g_variant_iter_init (&iter, extra_owned);
+  while (g_variant_iter_next (&iter, "{&s&s}", &key, &val))
+    {
+      if (g_str_equal (key, "my-custom-key"))
+        {
+          g_assert_cmpstr (val, ==, "some-value");
+          found_custom = TRUE;
+        }
+      else if (g_str_equal (key, "x-options-source-tuned"))
+        {
+          g_assert_cmpstr (val, ==, "nohz=full");
+          found_tuned = TRUE;
+        }
+      else
+        g_assert_not_reached ();
+    }
+  g_assert_true (found_custom);
+  g_assert_true (found_tuned);
+
+  g_variant_unref (extra_owned);
+  g_object_unref (parser);
+}
+
+static void
+test_extra_keys_roundtrip (void)
+{
+  /* Test that extra keys can be serialized to a variant and restored */
+  OstreeBootconfigParser *original = ostree_bootconfig_parser_new ();
+  ostree_bootconfig_parser_set (original, "options", "root=UUID=abc rw");
+  ostree_bootconfig_parser_set (original, "linux", "/vmlinuz");
+  ostree_bootconfig_parser_set (original, "x-options-source-tuned", "nohz=full isolcpus=1-3");
+
+  /* Serialize */
+  GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (original);
+  g_assert_nonnull (extra);
+  GVariant *extra_owned = g_variant_ref_sink (extra);
+
+  /* Create a new parser (simulating deserialization) with only standard keys */
+  OstreeBootconfigParser *restored = ostree_bootconfig_parser_new ();
+  ostree_bootconfig_parser_set (restored, "options", "root=UUID=abc rw nohz=full isolcpus=1-3");
+
+  /* Restore extra keys from variant */
+  GVariantIter iter;
+  const char *key, *value;
+  g_variant_iter_init (&iter, extra_owned);
+  while (g_variant_iter_next (&iter, "{&s&s}", &key, &value))
+    ostree_bootconfig_parser_set (restored, key, value);
+
+  /* Verify the extension key survived the roundtrip */
+  g_assert_cmpstr (ostree_bootconfig_parser_get (restored, "x-options-source-tuned"), ==,
+                   "nohz=full isolcpus=1-3");
+  /* Standard keys should also be present */
+  g_assert_cmpstr (ostree_bootconfig_parser_get (restored, "options"), ==,
+                   "root=UUID=abc rw nohz=full isolcpus=1-3");
+
+  g_variant_unref (extra_owned);
+  g_object_unref (original);
+  g_object_unref (restored);
+}
+
+static void
+test_extra_keys_parse_write_roundtrip (void)
+{
+  /* Test that x-prefixed keys survive a parse -> write -> parse roundtrip
+   * via the BLS file format.
+   */
+  const char *bls_content = "title Fedora Linux 43\n"
+                            "version 6.8.0-300.fc40.x86_64\n"
+                            "linux /vmlinuz-6.8.0\n"
+                            "initrd /initramfs-6.8.0.img\n"
+                            "options root=UUID=abc rw nohz=full\n"
+                            "x-options-source-tuned nohz=full\n";
+
+  /* Write the BLS content to a temp file */
+  g_autofree char *tmpdir = g_dir_make_tmp ("ostree-test-XXXXXX", NULL);
+  g_assert_nonnull (tmpdir);
+  g_autofree char *tmpfile = g_build_filename (tmpdir, "ostree-test.conf", NULL);
+  g_assert_true (g_file_set_contents (tmpfile, bls_content, -1, NULL));
+
+  /* Parse */
+  OstreeBootconfigParser *parser = ostree_bootconfig_parser_new ();
+  g_assert_true (ostree_bootconfig_parser_parse_at (parser, AT_FDCWD, tmpfile, NULL, NULL));
+
+  /* The x-prefixed key should have been parsed */
+  g_assert_cmpstr (ostree_bootconfig_parser_get (parser, "x-options-source-tuned"), ==,
+                   "nohz=full");
+  g_assert_cmpstr (ostree_bootconfig_parser_get (parser, "options"), ==,
+                   "root=UUID=abc rw nohz=full");
+
+  /* Write it back out */
+  g_autofree char *outfile = g_build_filename (tmpdir, "ostree-test-out.conf", NULL);
+  g_assert_true (ostree_bootconfig_parser_write_at (parser, AT_FDCWD, outfile, NULL, NULL));
+
+  /* Parse the output and verify the key survived */
+  OstreeBootconfigParser *parser2 = ostree_bootconfig_parser_new ();
+  g_assert_true (ostree_bootconfig_parser_parse_at (parser2, AT_FDCWD, outfile, NULL, NULL));
+  g_assert_cmpstr (ostree_bootconfig_parser_get (parser2, "x-options-source-tuned"), ==,
+                   "nohz=full");
+  g_assert_cmpstr (ostree_bootconfig_parser_get (parser2, "options"), ==,
+                   "root=UUID=abc rw nohz=full");
+
+  g_object_unref (parser);
+  g_object_unref (parser2);
+  (void)unlink (tmpfile);
+  (void)unlink (outfile);
+  (void)rmdir (tmpdir);
+}
+
 int
 main (int argc, char *argv[])
 {
@@ -56,5 +261,13 @@ main (int argc, char *argv[])
 
   g_test_add_func ("/bootconfig-parser/tries/valid", test_parse_tries_valid);
   g_test_add_func ("/bootconfig-parser/tries/invalid", test_parse_tries_invalid);
+  g_test_add_func ("/bootconfig-parser/extra-keys/empty", test_extra_keys_variant_empty);
+  g_test_add_func ("/bootconfig-parser/extra-keys/with-extension-keys",
+                   test_extra_keys_variant_with_extension_keys);
+  g_test_add_func ("/bootconfig-parser/extra-keys/standard-excluded",
+                   test_extra_keys_variant_standard_excluded);
+  g_test_add_func ("/bootconfig-parser/extra-keys/roundtrip", test_extra_keys_roundtrip);
+  g_test_add_func ("/bootconfig-parser/extra-keys/parse-write-roundtrip",
+                   test_extra_keys_parse_write_roundtrip);
   return g_test_run ();
 }